﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;

using MySql.Data.MySqlClient;
using System.Windows.Forms;

namespace MMOShared
{
    public class FileUpdateStatistics
    {
        public int NumberOfFiles { get; set; }
        public int NewFiles { get; set; }
        public int FilesUpdated { get; set; }
        public int NumberOfPieces { get; set; }
        public long TotalSize { get; set; }
        public int Deletions { get; set; }
    }

    public static class FileUpdater
    {
        public static bool VerifySuccessfulUpdate(List<FileUpdatePiece> upPieces, string origFile, string updatedFile)
        {
            //this is a debug function for ensuring that file updating works

            //create a byte array containing the entire updated file
            FileStream updatedFileS = new FileStream(updatedFile, FileMode.Open, FileAccess.Read, FileShare.None);
            List<byte> upFileB = new List<byte>();
            int b = 0;
            while ((b = updatedFileS.ReadByte()) != -1)
            {
                upFileB.Add((byte)b);
            }
            updatedFileS.Close();

            //create a byte array containing the updated file as it would be if it were sent as a list of file update pieces
            List<byte> createdFileB = new List<byte>();
            List<FileUpdatePiece> allPieces = BuildFileListWithUpdates(upPieces, origFile);
            foreach (FileUpdatePiece p in allPieces)
            {
                createdFileB.AddRange(p.Data);
            }

            //test same size
            if (upFileB.Count != createdFileB.Count)
            {
                MessageBox.Show("File update failed: Size mismatch");
                //return false;
            }

            //test same contents
            for (int i = 0; i < upFileB.Count; i++)
            {
                if (upFileB[i] != createdFileB[i])
                {
                    MessageBox.Show("File update failed: Byte mismatch at the " + i.ToString() + "th byte ["+upFileB[i-1]+", "+upFileB[i]+", "+upFileB[i+1]+" | "+createdFileB[i-1]+", "+createdFileB[i]+", "+createdFileB[i+1]+"]");
                    return false;
                }
            }
            //Success!
            return true;
        }

        public static bool UpdateFileFromList(List<FileUpdatePiece> upPieces, string filePath)
        {
            //Get the updated file as a list of the total of its pieces
            List<FileUpdatePiece> totalFile = BuildFileListWithUpdates(upPieces, filePath);
            //If this file is in a directory that doesn't exist, make it
            String[] dirs = filePath.Split(new String[] { "\\" }, 999, StringSplitOptions.None);
            string pathSoFar = "";
            int startCreateAt = 1000;
            for (int i = 0; i < dirs.Length - 1; i++)
            {
                string currDir = dirs[i];
                pathSoFar += currDir + "\\";
                if (currDir == "Dwld_Tmp")
                    startCreateAt = i;
                else if (i > startCreateAt)
                {
                    if (!Directory.Exists(pathSoFar))
                    {
                        Directory.CreateDirectory(pathSoFar);
                    }
                }
            }


            FileStream newFile = null;
            try
            {
                newFile = new FileStream(filePath, FileMode.OpenOrCreate, FileAccess.Write, FileShare.None);
                foreach (FileUpdatePiece piece in totalFile)
                {
                    newFile.Write(piece.Data, 0, (int)piece.Size);
                }
                newFile.Close();
            }
            catch (Exception e)
            {
                MessageBox.Show("Error: " + e.Message);
                newFile.Close();
                return false;
            }
            return true;
        }
        private static List<FileUpdatePiece> BuildFileListWithUpdates(List<FileUpdatePiece> upPieces,string filePath)
        {
            List<FileUpdatePiece> totalFilePieces = new List<FileUpdatePiece>();
            if (!File.Exists(filePath))
            {
                totalFilePieces = upPieces;
            }
            else
            {
                FileStream updateFile = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.None);
                FileUpdatePiece lastUpdatePiece = null;



                foreach (FileUpdatePiece piece in upPieces)
                {
                    //Make a piece containing the begining of the file up to the first updated piece
                    if (lastUpdatePiece == null)
                    {
                        if (piece.StartByte > 0)
                        {
                            long size = piece.StartByte - 1;
                            FileUpdatePiece firstPiece = new FileUpdatePiece(piece.ResourceID, 0, size, false);
                            byte[] buff = new byte[size];
                            updateFile.Read(buff,0,(int)size);
                            firstPiece.Data = buff;
                            totalFilePieces.Add(firstPiece);
                        }
                        if (!piece.IsDelete)
                            totalFilePieces.Add(piece);

                        lastUpdatePiece = piece;
                    }
                    else
                    {
                        //Make a piece containing the unchanged data between two update pieces
                        if (lastUpdatePiece.Size + lastUpdatePiece.StartByte + 1 < piece.StartByte)
                        {
                            //only add the unchanged piece if it exists
                            long startByte = lastUpdatePiece.StartByte + lastUpdatePiece.Size + 1;
                            long size = piece.StartByte - startByte;
                            FileUpdatePiece inbetweenPiece = new FileUpdatePiece(piece.ResourceID,
                                startByte, size, false);
                            if (updateFile.Seek(startByte, SeekOrigin.Begin) != startByte)
                                MessageBox.Show("Seek error in update file");
                            byte[] buff = new byte[size];
                            updateFile.Read(buff, 0, (int)size);
                            inbetweenPiece.Data = buff;
                            totalFilePieces.Add(inbetweenPiece);
                        }
                        if (!piece.IsDelete)
                            totalFilePieces.Add(piece);

                        lastUpdatePiece = piece;
                    }

                }
                //There may be a piece of unchanged data after the last updated piece
                FileUpdatePiece lastPiece = upPieces[upPieces.Count - 1];
                if (((lastPiece.StartByte + lastPiece.Size) < updateFile.Length) && !lastPiece.IsDelete)
                {
                    long startByte = lastPiece.StartByte + lastPiece.Size + 1;
                    long size = updateFile.Length - startByte;
                    FileUpdatePiece lastUnchangedPiece = new FileUpdatePiece(lastPiece.ResourceID, startByte, size, false);
                    if (updateFile.Seek(startByte, SeekOrigin.Begin) != startByte)
                        MessageBox.Show("Seek error in update file");
                    byte[] buff = new byte[size];
                    updateFile.Read(buff, 0, (int)size);
                    lastUnchangedPiece.Data = buff;
                    totalFilePieces.Add(lastUnchangedPiece);
                }
                updateFile.Close();
            }
            return totalFilePieces;
        }
    }

    [Serializable]
    public class FileUpdatePiece
    {
        private byte[] _data;
        public string ResourceID { get; private set; }
        public long StartByte { get; set; }
        public long Size { get; set; }
        public byte[] Data { get { return _data; } set { if (!IsDelete) { if (value == null) _data = null; else { Array.Copy(value, _data, value.Length); } } } }
        public bool IsDelete { get; private set; }

        public FileUpdatePiece(string resID, long startByte, long size, bool isDelete)
        {
            ResourceID = resID;
            StartByte = startByte;
            Size = size;
            _data = new byte[size];
            IsDelete = isDelete;
        }

        public static FileUpdateStatistics CreateUpdateDiffSQL(MySqlConnection conn,ResourceManager oldVersion,ResourceManager newVersion)
        {
            FileUpdateStatistics stats = new FileUpdateStatistics();

            //first create table sql
            MySqlCommand command = conn.CreateCommand();
            command.Connection = conn;
            string sqlQuery = "CREATE  TABLE `mmo_test_game`.`update_"+newVersion.Version.ToString()+"` ( " +
                        "`piece_id` INT UNSIGNED NOT NULL AUTO_INCREMENT , " +
                        "`piece_resource` VARCHAR(45) NOT NULL , " +
                        "`piece_start_byte` BIGINT UNSIGNED NOT NULL , " +
                        "`piece_size` BIGINT UNSIGNED NOT NULL , " +
                        "`piece_delete` BIT NOT NULL , " +
                        "`piece_data` LONGBLOB NOT NULL , " +
                        "PRIMARY KEY (`piece_id`) , " +
                        "UNIQUE INDEX `piece_id_UNIQUE` (`piece_id` ASC) ) " +
                        "ENGINE = MyISAM;";
            command.CommandText = sqlQuery;
            command.ExecuteNonQuery();

            //populate update table
            foreach(ResourceEntry newEntry in newVersion.Resources){
                stats.NumberOfFiles++;
                /*bool fileIsUpdated = false;
                ResourceEntry oEntry = null;
                //check for files that existed in the old version
                if (oldVersion != null)
                {
                    foreach (ResourceEntry oldEntry in oldVersion.Resources)
                    {
                        if (oldEntry.ResourceID == newEntry.ResourceID)
                        {
                            oEntry = oldEntry;
                            fileIsUpdated = true;
                            break;
                        }
                    }
                }
                if(fileIsUpdated && oEntry != null){
                    //resource exists in both version
                    stats.FilesUpdated++;
                    CreateDiffResourceSQL(conn,oEntry,newEntry, newVersion.Version,stats);
                } else {*/
                    //resource is new to this version
                    //TEMPORARY : Don't use diff based updates, just send the whole file
                    stats.NewFiles++;
                    CreateNewResourceSQL(conn,newEntry,newVersion.Version.ToString(),stats);
                //}
            }
            
            //add an entry in the updates table pointing to the new update's table
            string ver = newVersion.Version.ToString();
            sqlQuery = "INSERT INTO `mmo_test_game`.`updates` (`update_version`, `update_table`, `update_size`, `update_piece_count`, `update_file_count`) VALUES ('"+ver+"', 'update_"+ver+"', '"+stats.TotalSize+"', '"+stats.NumberOfPieces+"', '"+stats.NumberOfFiles+"');";
            command.CommandText = sqlQuery;
            command.ExecuteNonQuery();

            return stats;
        }

        private static void CreateEndOfFilePiece(long oFileLen,long nFileLen,string resID,bool isDelete,List<FileUpdatePiece> updateL,FileUpdateStatistics stats){
            long pSize = (UInt32)(oFileLen - nFileLen);
            if (!isDelete) pSize = (UInt32)(nFileLen - oFileLen);
            long pStart = (UInt32)(nFileLen - 1);
            if (pSize == 0)
            {
                MessageBox.Show("[CreateEndOfFilePiece] Size 0! At " + pStart);
            }
            FileUpdatePiece p = new FileUpdatePiece(resID, pStart, pSize, isDelete);
            updateL.Add(p);
            if (isDelete)
                stats.Deletions++;
            stats.NumberOfPieces++;
            stats.TotalSize += pSize;
        }

        private static void FinishPiece(long currentByte,long startByte,string resID,List<FileUpdatePiece> updateL,FileUpdateStatistics stats) {
            //Finish current piece
            long pAddSize = currentByte - startByte;
            if (pAddSize == 0)
            {
                MessageBox.Show("[FinishPiece] Size 0! At " + startByte);
            }
            FileUpdatePiece addP = new FileUpdatePiece(resID, startByte, pAddSize, false);
            updateL.Add(addP);
            stats.NumberOfPieces++;
            stats.TotalSize += pAddSize;
        }

        private static void CreateDiffResourceSQL(MySqlConnection conn, ResourceEntry oRes, ResourceEntry nRes, ushort newVersion, FileUpdateStatistics stats)
        {
            /*
             * INSERT INTO `mmo_test_game`.`update_1` (`piece_resource`, `piece_start_byte`, `piece_size`, `piece_data`) VALUES ('temp_resourceID', '0', '1337', ?);
             */
            string nFileLoc = Path.Combine("Updates\\" + newVersion.ToString() + "\\", nRes.RelativePath);
            string oFileLoc = Path.Combine("Updates\\" + (newVersion-1).ToString() + "\\", oRes.RelativePath);
            FileStream nFile = new FileStream(nFileLoc, FileMode.Open, FileAccess.Read);
            FileStream oFile = new FileStream(oFileLoc, FileMode.Open, FileAccess.Read);
            UInt32 buffSize = 1024 * 10;

            Int32 n = 0;
            Int32 o = 0;
            long i = 0;

            List<FileUpdatePiece> updateL = new List<FileUpdatePiece>();

            bool isDiffStarted = false;
            long startByte = 0;

            string resID = nRes.ResourceID;
            while (true)
            {
                n = nFile.ReadByte();
                o = oFile.ReadByte();
                if (n == -1 && o != -1)
                {
                    if (isDiffStarted)
                    {
                        //we are midway through creating a piece, so finish it off
                        FinishPiece(i,startByte,resID,updateL,stats);
                    }
                    //new res ends before old res. Create a delete file portion message
                    CreateEndOfFilePiece(oFile.Length,nFile.Length,resID,true,updateL,stats);
                    break;
                }
                else if (o == -1 && n != -1)
                {
                    if (isDiffStarted)
                    {
                        //we are midway through creating a piece, so split the copy into two pieces
                        FinishPiece(i,startByte,resID,updateL,stats);
                    }
                    //old res ends before new res. Create an add file portion message for the data outside the overlap
                    CreateEndOfFilePiece(oFile.Length,nFile.Length,resID,false,updateL,stats);
                    break;
                }
                else if (n == -1 && o == -1)
                {
                    if (isDiffStarted)
                    {
                        //we are midway through creating a piece, so split the copy into two pieces
                        FinishPiece(i,startByte,resID,updateL,stats);
                    }
                    //we have reached the end of both files, break loop
                    break;
                }
                else if ((byte)n == (byte)o)
                {
                    //bytes are the same
                    if (isDiffStarted)
                    {
                        //end copy piece
                        FinishPiece(i,startByte,resID,updateL,stats);
                        isDiffStarted = false;
                    }
                }
                else if ((byte)n != (byte)o)
                {
                    //bytes are different
                    if (!isDiffStarted)
                    {
                        //start copy piece
                        startByte = i;
                        isDiffStarted = true;
                    }
                }
                i++; //goto next byte
            }
            //We have found the differences so we are done with the old version's file
            oFile.Close();

            //Make all data carrying pieces have a max data size of buffSize, if a piece has a data size greater
            // than that, then split the original piece into parts
            int j = 0;
            for(int ind = 0; i < updateL.Count; i++)
            {
                FileUpdatePiece fup = updateL[ind];
                if (!fup.IsDelete)
                { //only resize pieces with data attached
                    for (UInt32 k = 1; k * buffSize < fup.Size; k++)
                    {
                        if (fup.Size > buffSize)
                        {
                            //Create a piece for the first buffSize of bytes and add it before the piece
                            FileUpdatePiece firstHalf = new FileUpdatePiece(fup.ResourceID, fup.StartByte, buffSize, false);
                            int startIndex = j - 1; if (j == 0) { startIndex = 0; }
                            updateL.Insert(startIndex,firstHalf); //TODO CONTINUE HERE
                            //Now shift what remains of the piece right by buffSize bytes
                            fup.StartByte += buffSize;
                            fup.Size -= buffSize;
                            if (fup.Size == 0)
                            {
                                MessageBox.Show("[PieceDownsizing] Size 0! At new start byte, " + fup.StartByte);
                            }
                            stats.NumberOfPieces++;
                        }
                        j++;
                    }
                }
            }

            //Read pertinent portions of the file and serialize it to SQL
            foreach (FileUpdatePiece fup in updateL)
            {
                if (!fup.IsDelete)
                {
                    MySqlCommand comm = conn.CreateCommand();
                    comm.Connection = conn;
                    if (nFile.Seek(fup.StartByte, SeekOrigin.Begin) == fup.StartByte)
                    {
                        byte[] buff = new byte[fup.Size];
                        nFile.Read(buff, 0, (int)fup.Size);

                        int delete = 0;
                        if (fup.IsDelete) delete = 1;

                        string sqlQuery = "INSERT INTO `mmo_test_game`.`update_" + newVersion.ToString() + "` (`piece_resource`, " +
                            "`piece_start_byte`, `piece_size`, `piece_delete`, `piece_data`) VALUES ('" + fup.ResourceID + "', " +
                            "'" + fup.StartByte.ToString() + "', '" + fup.Size.ToString() + "', b'"+delete.ToString()+"', ?piece_data);";

                        MySqlParameter param = comm.Parameters.Add("?piece_data", MySqlDbType.LongBlob);
                        param.Value = buff;

                        comm.CommandText = sqlQuery;
                        comm.ExecuteNonQuery();
                    }
                    else
                    {
                        MessageBox.Show("Error: could not seek resource file");
                    }
                }
            }

            nFile.Close();
        }

        private static void CreateNewResourceSQL(MySqlConnection conn,ResourceEntry newRes,string newVersion,FileUpdateStatistics stats)
        {
            /*
             * INSERT INTO `mmo_test_game`.`update_1` (`piece_resource`, `piece_start_byte`, `piece_size`, `piece_data`) VALUES ('temp_resourceID', '0', '1337', ?);
             */
            string newFileLoc = Path.Combine("Updates\\" + newVersion + "\\", newRes.RelativePath);
            FileStream nFile = new FileStream(newFileLoc, FileMode.Open, FileAccess.Read);
            int buffSize = 1024 * 10;

            byte[] nBuff = new byte[buffSize];
            int i = 0;
            int nBytesRead = 0;
            while ((nBytesRead = nFile.Read(nBuff, 0, buffSize)) > 0)
            {
                MySqlCommand command = conn.CreateCommand();
                command.Connection = conn;

                string startByte = (i * buffSize).ToString();
                string size = nBytesRead.ToString();

                stats.NumberOfPieces++;
                stats.TotalSize += nBytesRead;

                string sqlQuery = "INSERT INTO `mmo_test_game`.`update_" + newVersion + "` (`piece_resource`, " +
                "`piece_start_byte`, `piece_size`, `piece_delete`, `piece_data`) VALUES ('" + newRes.ResourceID + "', " +
                "'" + startByte + "', '" + size + "', b'0', ?piece_data_value);";

                MySqlParameter dataParam = command.Parameters.Add("?piece_data_value", MySqlDbType.LongBlob);
                dataParam.Value = nBuff;

                command.CommandText = sqlQuery;
                command.ExecuteNonQuery();

                i++;
            }
            nFile.Close();
        }
    }
}

/*

CREATE  TABLE `mmo_test_game`.`update_1` (

  `piece_id` INT UNSIGNED NOT NULL AUTO_INCREMENT ,

  `piece_resource` VARCHAR(45) NOT NULL ,

  `piece_start_byte` INT UNSIGNED NOT NULL ,

  `piece_size` INT UNSIGNED NOT NULL ,

  `piece_data` LONGBLOB NOT NULL ,

  PRIMARY KEY (`piece_id`) ,

  UNIQUE INDEX `piece_id_UNIQUE` (`piece_id` ASC) )

ENGINE = MyISAM; */